﻿using System;
using System.IO;
using System.Net;
using System.Text;
using System.Threading;
using HashFoo.Core;

namespace HashFooRest.App.Client
{
    /// <summary>
    /// <see cref="IWebClient"/> based on a <see cref="WebRequest"/> infrastructure.
    /// </summary>
    public class WebRequestClient : IWebClient
    {
        readonly string _baseUri;
        readonly Action<Action> _callbackMarshaller;
        readonly WebHeaderCollection _defaultHeaders = new WebHeaderCollection();
        //readonly CookieContainer _cookies = new CookieContainer();

        /// <summary>
        /// The size, in bytes, of the buffer to use for writing to the web request streamm.
        /// </summary>
        public int WriteBufferSize { get; set; }

        /// <summary>
        /// The size, in bytes, of the read buffer to use for reading a response stream.
        /// </summary>
        public int ReadBufferSize { get; set; }

        /// <summary>
        /// Create a new client with an optional base uri.
        /// </summary>
        /// <param name="baseUri">Optional base uri to prepend to any request Uri that is not absolute.</param>
        /// <param name="callbackMarshaller">Optional delegate to marshal callbacks through.</param>
        public WebRequestClient(string baseUri = null, Action<Action> callbackMarshaller = null)
        {
            _baseUri = baseUri;
            _callbackMarshaller = callbackMarshaller ?? (x => x());

            WriteBufferSize = 1024 * 128;
            ReadBufferSize = 1024 * 128;
        }

        /// <summary>
        /// The headers that will be part of every request.
        /// </summary>
        public WebHeaderCollection DefaultHeaders
        {
            get { return _defaultHeaders; }
        }

        /// <summary>
        /// Holds the cookies for the current web client.  Cookies are kept between requests for each client.
        /// </summary>
        //public CookieContainer Cookies
        //{
        //    //get { return _cookies; }
        //}

        /// <summary>
        /// Perform a web request and return the result as the raw response stream.
        /// </summary>
        /// <param name="uri">The uri to call</param>
        /// <param name="successCallback">Callback for successful completion</param>
        /// <param name="errorCallback">Callback if there is an error</param>
        /// <param name="cancelCallback">Callback if the operation is canceled</param>
        /// <param name="progressCallback">Callback for progress updates.</param>
        /// <param name="method">HTTP method override.</param>
        /// <param name="buffer">
        ///   If <c>true</c>, the network stream will be downloaded completely, then wrapped in a memory stream.
        ///   Else, the network stream will be returned.  Default to true.
        /// </param>
        /// <returns>The operation, useful to cancel if needed.</returns>
        /// <remarks>
        /// If the stream is not buffered, the <paramref name="progressCallback"/> will not be invoked.
        /// Buffering should probably be turned off for very large files (>10MBs).
        /// </remarks>
        public IWebClientOperation DownloadStreamAsync(Uri uri, Action<IWebResultStream> successCallback, Action<WebRequestError> errorCallback = null, Action cancelCallback = null, Action<ProgressEventArgs> progressCallback = null, string method = null, bool buffer = true)
        {
            var request = CreateHttpWebRequest(uri, method ?? "GET");
            var op = BuildOp(request, cancelCallback);

            // wrap our callbacks so that they mark the operations as completed
            var opErrorCallback = CreateSafeOpErrorCallback(op, errorCallback);
            var opSuccessCallback = CreateSafeOpCallback(op, successCallback);
            var marshalledProgressCallback = CreateMarshalledProgressCallback(progressCallback);

            // create handlers and callbacks for the request and responses.
            Action<HttpWebResponse> responseHandler = r => opSuccessCallback(new WebResultStream(r, progressCallback: marshalledProgressCallback, streamBufferSize: ReadBufferSize));
            Action<Func<HttpWebResponse>> responseCallback = respProvider => SafeProcessResponse(respProvider, responseHandler, opErrorCallback);

            ThreadPool.QueueUserWorkItem(x => SafeHandleResponseAsync(request, responseCallback, opErrorCallback));
            return op;
        }

        /// <summary>
        /// Performs a web request that uploads data from a stream.  Any server response is returned as a string.
        /// </summary>
        /// <param name="uri">The uri to call</param>
        /// <param name="data">The data to upload</param>
        /// <param name="successCallback">Callback for successful completion</param>
        /// <param name="errorCallback">Callback if there is an error</param>
        /// <param name="cancelCallback">Callback if the operation is canceled</param>
        /// <param name="progressCallback">Callback for progress updates of the upload</param>
        /// <param name="method">HTTP method override.</param>
        /// <returns>The operation, useful to cancel if needed.</returns>
        public IWebClientOperation UploadStreamAsync(Uri uri, Stream data, Action<IWebResultString> successCallback = null, Action<WebRequestError> errorCallback = null, Action cancelCallback = null, Action<ProgressEventArgs> progressCallback = null, string method = null)
        {
            var request = CreateHttpWebRequest(uri, method ?? "POST");
            var op = BuildOp(request, cancelCallback);

            // wrap our callbacks so that they mark the operations as completed
            var opErrorCallback = CreateSafeOpErrorCallback(op, errorCallback);
            var opSuccessCallback = CreateSafeOpCallback(op, successCallback);

            // create handlers and callbacks for the request and responses.
            Action<HttpWebResponse> successResponseHandler = response => opSuccessCallback(new WebResultString(response));
            Action<Func<HttpWebResponse>> responseCallback = responseProvider => SafeProcessResponse(responseProvider, successResponseHandler, opErrorCallback);
            Action<Func<Stream>> requestStreamCallback = streamProvider =>
            {
                // upload the string (synchronous)
                SafeUploadStream(streamProvider, data, opErrorCallback, progressCallback);

                // op will be done if canceled or error.
                if (op.IsDone) return;

                // after a successful upload, get the response from the server and handle it.
                SafeHandleResponseAsync(request, responseCallback, opErrorCallback);
            };

            ThreadPool.QueueUserWorkItem(x => SafeHandleRequestStreamAsync(request, requestStreamCallback, opErrorCallback));
            return op;
        }

        /// <summary>
        /// Performs a web request that uploads data from a byte array.  Any server response is returned as a string.
        /// </summary>
        /// <param name="uri">The uri to call</param>
        /// <param name="data">The data to upload</param>
        /// <param name="successCallback">Callback for successful completion</param>
        /// <param name="errorCallback">Callback if there is an error</param>
        /// <param name="cancelCallback">Callback if the operation is canceled</param>
        /// <param name="progressCallback">Callback for progress updates of the upload</param>
        /// <param name="method">HTTP method override.</param>
        /// <returns>The operation, useful to cancel if needed.</returns>
        public IWebClientOperation UploadBytesAsync(Uri uri, byte[] data, Action<IWebResultString> successCallback = null, Action<WebRequestError> errorCallback = null, Action cancelCallback = null, Action<ProgressEventArgs> progressCallback = null, string method = null)
        {
            return UploadStreamAsync(
                uri,
                new MemoryStream(data),
                successCallback,
                errorCallback,
                cancelCallback,
                progressCallback,
                method);
        }

        /// <summary>
        /// Perform a web request and return the result as a string.
        /// </summary>
        /// <param name="uri">The uri to call</param>
        /// <param name="successCallback">Callback for successful completion</param>
        /// <param name="errorCallback">Callback if there is an error</param>
        /// <param name="cancelCallback">Callback if the operation is canceled</param>
        /// <param name="progressCallback"></param>
        /// <param name="method">HTTP method override.</param>
        /// <returns>The operation, useful to cancel if needed.</returns>
        public IWebClientOperation DownloadStringAsync(Uri uri, Action<IWebResultString> successCallback, Action<WebRequestError> errorCallback = null, Action cancelCallback = null, Action<ProgressEventArgs> progressCallback = null, string method = null)
        {
            var request = CreateHttpWebRequest(uri, method ?? "GET");
            var op = BuildOp(request, cancelCallback);

            // wrap our callbacks so that they mark the operations as completed
            var opErrorCallback = CreateSafeOpErrorCallback(op, errorCallback);
            var opSuccessCallback = CreateSafeOpCallback(op, successCallback);

            // wrap the progress callback through the marshaller
            var marshalledProgressCallback = CreateMarshalledProgressCallback(progressCallback);

            // create handlers and callbacks for the request and responses.
            Action<HttpWebResponse> responseHandler = r => opSuccessCallback(new WebResultString(r, marshalledProgressCallback));
            Action<Func<HttpWebResponse>> responseCallback = respProvider => SafeProcessResponse(respProvider, responseHandler, opErrorCallback);

            ThreadPool.QueueUserWorkItem(x => SafeHandleResponseAsync(request, responseCallback, opErrorCallback));
            return op;
        }

        /// <summary>
        /// Performs a web request that uploads data as a string.  Any server response is returned as a string.
        /// </summary>
        /// <param name="uri">The uri to call</param>
        /// <param name="data">The data to upload</param>
        /// <param name="successCallback">Callback for successful completion</param>
        /// <param name="errorCallback">Callback if there is an error</param>
        /// <param name="cancelCallback">Callback if the operation is canceled</param>
        /// <param name="method">HTTP method override.</param>
        /// <returns>The operation, useful to cancel if needed.</returns>
        public IWebClientOperation UploadStringAsync(Uri uri, string data, Action<IWebResultString> successCallback = null, Action<WebRequestError> errorCallback = null, Action cancelCallback = null, string method = null)
        {
            var request = CreateHttpWebRequest(uri, method ?? "POST");
            var op = BuildOp(request, cancelCallback);

            // wrap our callbacks so that they mark the operations as completed
            var opErrorCallback = CreateSafeOpErrorCallback(op, errorCallback);
            var opSuccessCallback = CreateSafeOpCallback(op, successCallback);

            // create handlers and callbacks for the request and responses.
            Action<HttpWebResponse> completedResponseHandler = response => opSuccessCallback(new WebResultString(response));
            Action<Func<HttpWebResponse>> responseCallback = responseProvider => SafeProcessResponse(responseProvider, completedResponseHandler, opErrorCallback);
            Action<Func<Stream>> requestStreamCallback = streamProvider =>
            {
                // upload the string (synchronous)
                SafeUploadString(streamProvider, data, opErrorCallback);
                if (op.IsDone) return;

                // after a successful upload, get the response from the server and handle it.
                SafeHandleResponseAsync(request, responseCallback, opErrorCallback);
            };

            ThreadPool.QueueUserWorkItem(x => SafeHandleRequestStreamAsync(request, requestStreamCallback, opErrorCallback));
            return op;
        }

        /// <summary>
        /// Gets the request stream to the server, wrapped in a safe execute block.
        /// </summary>
        /// <param name="request"></param>
        /// <param name="handler"></param>
        /// <param name="errorCallback"></param>
        static void SafeHandleRequestStreamAsync(HttpWebRequest request, Action<Func<Stream>> handler, Action<WebRequestError> errorCallback)
        {
            WebUtil.SafeWebExecute(
                () =>
                request.BeginGetRequestStream(ar => handler(() => request.EndGetRequestStream(ar)), null),
                errorCallback);
        }

        /// <summary>
        /// Executes a request within a wrapper that will catch errors and invoke an error callback.
        /// </summary>
        /// <param name="request"></param>
        /// <param name="responseHandler"></param>
        /// <param name="errorCallback"></param>
        static void SafeHandleResponseAsync(HttpWebRequest request, Action<Func<HttpWebResponse>> responseHandler, Action<WebRequestError> errorCallback)
        {
            WebUtil.SafeWebExecute(
                () =>
                request.BeginGetResponse(ar => responseHandler(() => (HttpWebResponse)request.EndGetResponse(ar)), null),
                errorCallback);
        }

        /// <summary>
        /// Processes a web request response in a wrapper that catches errors and invokes an error callback.
        /// </summary>
        /// <param name="responseProvider">The function that gets the response.</param>
        /// <param name="responseHandler">The handler for the response.</param>
        /// <param name="errorCallback">The callback if there is an error.</param>
        void SafeProcessResponse(Func<HttpWebResponse> responseProvider, Action<HttpWebResponse> responseHandler, Action<WebRequestError> errorCallback)
        {
            if (errorCallback == null) throw new ArgumentNullException("errorCallback");
            if (responseHandler == null) throw new ArgumentNullException("responseHandler");

            WebUtil.SafeWebExecute(() =>
            {
                // get the response from the provider function
                var response = responseProvider();

                //if (response.Cookies != null)
                //{
                //    _cookies.Add(response.ResponseUri, response.Cookies);
                //}

                // if the response is not OK, call the error callback
                if (response.StatusCode != HttpStatusCode.OK) errorCallback(new WebRequestError(response));
                else responseHandler(response);
            }, errorCallback);
        }

        /// <summary>
        /// Uploads a string to the server.  If an error occurs, the errorCallback is invoked.
        /// </summary>
        /// <param name="streamProvider"></param>
        /// <param name="data"></param>
        /// <param name="errorCallback"></param>
        static void SafeUploadString(Func<Stream> streamProvider, string data, Action<WebRequestError> errorCallback)
        {
            WebUtil.SafeWebExecute(() =>
            {
                using (var requestStream = streamProvider())
                {
                    var dataArray = Encoding.UTF8.GetBytes(data);
                    requestStream.Write(dataArray, 0, dataArray.Length);
                }
            }, errorCallback);
        }

        /// <summary>
        /// Uploads a string to the server.  If an error occurs, the errorCallback is invoked.
        /// </summary>
        /// <param name="streamProvider"></param>
        /// <param name="data"></param>
        /// <param name="errorCallback"></param>
        /// <param name="progressCallback">Optional progress callback for the upload</param>
        void SafeUploadStream(Func<Stream> streamProvider, Stream data, Action<WebRequestError> errorCallback, Action<ProgressEventArgs> progressCallback = null)
        {
            WebUtil.SafeWebExecute(() =>
            {
                using (var requestStream = streamProvider())
                {
                    progressCallback = progressCallback ?? (p => { });
                    var dataLength = data.Length;
                    var readTotal = 0;

                    // 8KB write buffer
                    var buffer = new byte[WriteBufferSize];
                    var readCount = 0;
                    while ((readCount = data.Read(buffer, 0, buffer.Length)) > 0)
                    {
                        requestStream.Write(buffer, 0, readCount);
                        readTotal += readCount;

                        progressCallback(new ProgressEventArgs
                        {
                            Percent = (int)Math.Round(((double)readTotal / dataLength) * 100, 0),
                            Current = readTotal,
                            Total = dataLength,
                            TotalIsKnown = true
                        });
                    }

                }
            }, errorCallback);
        }

        /// <summary>
        /// Handles building the operation object that is returned by request methods.
        /// </summary>
        /// <param name="request"></param>
        /// <param name="cancelCallback"></param>
        /// <returns></returns>
        WebRequestClientOperation BuildOp(HttpWebRequest request, Action cancelCallback = null)
        {
            var op = new WebRequestClientOperation(request, () => _callbackMarshaller(cancelCallback));
            return op;
        }

        Uri BuildRequestUri(Uri uri)
        {
            if (uri.IsAbsoluteUri) return uri;
            return _baseUri == null ? uri : new Uri(new Uri(_baseUri, UriKind.Absolute), uri);
        }

        HttpWebRequest CreateHttpWebRequest(Uri uri, string method)
        {
            if (method == null) throw new ArgumentNullException("method");

            //WebRequest.CreateHttp(BuildRequestUri(uri));
            var request = (HttpWebRequest)System.Net.Browser.WebRequestCreator.BrowserHttp.Create(BuildRequestUri(uri)); 
            request.SetHeaders(_defaultHeaders);
            request.Method = method;
            //request.CookieContainer = Cookies;

            return request;
        }

        /// <summary>
        /// Creates a progress callback that is marshelled.
        /// </summary>
        /// <param name="progressCallback"></param>
        /// <returns></returns>
        Action<ProgressEventArgs> CreateMarshalledProgressCallback(Action<ProgressEventArgs> progressCallback)
        {
            if (progressCallback == null) return null;

            return p => _callbackMarshaller(() => progressCallback(p));
        }

        /// <summary>
        /// Wraps an  optional callback in an action that marks a <see cref="WebRequestClientOperation"/> as completed.
        /// </summary>
        /// <typeparam name="TResult"></typeparam>
        /// <param name="op"></param>
        /// <param name="callback"></param>
        /// <returns></returns>
        Action<TResult> CreateSafeOpCallback<TResult>(WebRequestClientOperation op, Action<TResult> callback)
        {
            if (callback == null) return r => op.MarkAsComplete();

            return r => op.MarkAsComplete(() => _callbackMarshaller(() => callback(r)));
        }

        /// <summary>
        /// Wraps an  optional error callback in an action that marks a <see cref="WebRequestClientOperation"/> as completed.
        /// If no action is supplied, an exception will be thrown after the operation has been marked as complete.
        /// </summary>
        /// <param name="op"></param>
        /// <param name="callback"></param>
        /// <returns></returns>
        Action<WebRequestError> CreateSafeOpErrorCallback(WebRequestClientOperation op, Action<WebRequestError> callback)
        {
            if (callback == null)
            {
                return r =>
                {
                    op.MarkAsComplete();
                    throw r.ErrorException;
                };
            }

            return r => op.MarkAsComplete(() => _callbackMarshaller(() => callback(r)));
        }
    }
}